/*
 * To change this template, choose Tools | Templates
 * and open the template in the editor.
 */
package edu.ncsu.csc573.project1.peer;

import edu.ncsu.csc573.project1.gui.MySplashScreen;
import edu.ncsu.csc573.project1.util.Constants;
import edu.ncsu.csc573.project1.util.Constants.PSMethod;
import edu.ncsu.csc573.project1.util.Constants.Status;
import edu.ncsu.csc573.project1.util.HeaderField;
import edu.ncsu.csc573.project1.util.PSRequest;
import edu.ncsu.csc573.project1.util.PSResponse;
import edu.ncsu.csc573.project1.util.PeerData;
import edu.ncsu.csc573.project1.util.Rfc;
import edu.ncsu.csc573.project1.util.RfcData;
import edu.ncsu.csc573.project1.util.RfcField;
import java.io.BufferedReader;
import java.io.DataInputStream;
import java.io.DataOutputStream;
import java.io.File;
import java.io.FileInputStream;
import java.io.IOException;
import java.io.InputStreamReader;
import java.io.ObjectInputStream;
import java.net.InetAddress;
import java.net.Socket;
import java.util.Date;
import java.util.HashMap;
import java.util.Iterator;
import java.util.LinkedList;
import java.util.Map;
import java.util.Random;
import java.util.concurrent.ConcurrentHashMap;
import java.util.regex.Matcher;
import java.util.regex.Pattern;
import javax.swing.JEditorPane;
import javax.swing.JFrame;
import javax.swing.JOptionPane;

/**
 *
 * @author svpendse1
 */
public class Peer {

    static String testName;
    static int testNumber;
    String peerHostName;            //The hostname of the peer
    Socket peerSocket;              //The socket for this peer
    String serverHostName;          //The server hostname
    int serverPortNumber;           //The server port number 
    DataInputStream in;              //Reader to read data from server
    DataOutputStream out;                //Writer to write data to server
    ConcurrentHashMap<Integer, Rfc> rfcMap;   //The RFC map (RFC Number -> RFC) (thread-safe)
    UploadServer uploadServer;      //The upload server for this peer
    int uploadServerPortNumber;     //The upload server port number
    boolean isConnected;    //Whether the peer is connected to a server
    JEditorPane uploadEditorPane;   //The upload server editor pane in the GUI
    
    public Peer(JEditorPane uploadEditorPane) {

        //Initialize the local RFC Map
        rfcMap = new ConcurrentHashMap<Integer, Rfc>();
        this.initializeRfcMap();
        
        this.serverHostName = Constants.kHostName;
        this.serverPortNumber = Constants.kPortNumber;
        this.uploadEditorPane = uploadEditorPane;
        
        try {
            this.peerHostName = InetAddress.getLocalHost().getHostAddress();
            System.out.println("Peer host name : " + peerHostName);
        } catch (Exception e) {
            e.printStackTrace();
        }
        
        this.uploadServerPortNumber = getFirstFreeLocalPort();
        if (uploadServerPortNumber == -1) {
            System.err.println("No free ports for the upload server on the host.");
        }
        
        this.startUploadServer();
    }

    public boolean isConnected() {
        return isConnected;
    }
    
    public void connectToServer() {
        isConnected = this.startConnection();
        if (isConnected) {
            this.sendRfcInformation();
        }
    }
    
    public ConcurrentHashMap<Integer, Rfc> getRfcMap() {
        return rfcMap;
    }
    
    private void sendRfcInformation() {
        //send the RFC information to the server
        //add it to the server's centralized index
        System.out.println("Sending RFC Information");
        Iterator it = rfcMap.entrySet().iterator();
        while (it.hasNext()) {
            Map.Entry<Integer, Rfc> pairs = (Map.Entry<Integer, Rfc>) it.next();
            PSRequest request = new PSRequest.PSRequestBuilder().exchangeMethod(PSMethod.ADD).hostField(new HeaderField("Host", peerHostName)).portField(new HeaderField("Port",  "" + uploadServerPortNumber)).rfcNumber(pairs.getKey().intValue()).titleField(new HeaderField("Title", pairs.getValue().getMetaData().getName())).version(Constants.kVersion).build();
            try {
                out.writeInt(request.getRequestMessage().length());
                out.writeBytes(request.getRequestMessage());
                //System.out.println("Message sent : " + request.getRequestMessage().length());
            } catch (Exception e) {
                e.printStackTrace();
            }
        }
    }
   
    public boolean addRfc(int rfcNumber, String rfcTitle, File rfcFile) {
        if (! isConnected) {
            connectToServer();
            if (! isConnected) {
                return false;
            }
        }
        if (! rfcMap.containsKey(new Integer(rfcNumber))) {
            RfcData metadata = new RfcData(rfcNumber, rfcTitle, this.peerHostName);
            String content = "";
            Rfc rfc;
            try {
                BufferedReader buf = new BufferedReader(new InputStreamReader(new FileInputStream(rfcFile)));
                String currentLine;
                while ((currentLine = buf.readLine()) != null) {
                    content += currentLine + "\n";
                }
            } catch (Exception e) {
            }
            rfc = new Rfc(metadata, content, new Date(rfcFile.lastModified()));
            rfcMap.put(new Integer(rfcNumber), rfc);                 
            PSRequest request = new PSRequest.PSRequestBuilder().exchangeMethod(PSMethod.ADD).hostField(new HeaderField("Host", peerHostName)).portField(new HeaderField("Port",  "" + uploadServerPortNumber)).rfcNumber(rfc.getMetaData().getNumber()).titleField(new HeaderField("Title", rfc.getMetaData().getName())).version(Constants.kVersion).build();
            try {
                out.writeInt(request.getRequestMessage().length());
                out.writeBytes(request.getRequestMessage());
                //System.out.println("Message sent : " + request.getRequestMessage().length());
            } catch (Exception e) {
                e.printStackTrace();
            }
            System.out.println(rfcFile.getName() + " added");
            return true;
        } else { 
            System.out.println(rfcFile.getName() + " already in peer's database");
            return false;
        }
    }
    
    public LinkedList<PeerData> lookupRfc(int rfcNumber,
                                           String title) {
        if (! isConnected) {
            connectToServer();
            if (! isConnected) {
                return null;
            }
        }
        LinkedList<PeerData> peers = new LinkedList<PeerData>();
        PSRequest request = new PSRequest.PSRequestBuilder().exchangeMethod(PSMethod.LOOKUP).hostField(new HeaderField("Host", peerHostName)).portField(new HeaderField("Port","" + uploadServerPortNumber)).rfcNumber(rfcNumber).titleField(new HeaderField("Title", title)).version(Constants.kVersion).build();
        try {
            out.writeInt(request.getRequestMessage().length());
            out.writeBytes(request.getRequestMessage());
        } catch (Exception e) {
            e.printStackTrace();
        }
    
        try {
            byte[] input = new byte[in.readInt()];
            in.readFully(input);
            String message = new String(input);
            PSResponse response = PSResponse.parseFromString(message);
            System.out.println(response.getResponseMessage());
            if (response.getStatus() == Status.OK) {
                for (int i = 0; i < response.getResponseList().size(); ++i) {
                    RfcField currentField = response.getResponseList().get(i);
                    peers.add(new PeerData(currentField.getHostName(), currentField.getUploadPortNumber()));
                    System.out.println("Found at : " + currentField.getHostName() + " " + currentField.getUploadPortNumber());
                }
                return peers;
            } else if (response.getStatus() == Status.NOT_FOUND) {
                System.out.println("The requested RFC was not found on any peer");
                JOptionPane.showMessageDialog(new JFrame(), "The requested RFC was not found on any peer", "Lookup Error", JOptionPane.ERROR_MESSAGE);
            } else if (response.getStatus() == Status.BAD_REQUEST) {
                System.out.println("A Bad Request");
                JOptionPane.showMessageDialog(new JFrame(), "A bad request occurred", "Bad Request", JOptionPane.ERROR_MESSAGE);
            } else if (response.getStatus() == Status.P2P_CI_VERSION_NOT_SUPPORTED) {
                System.out.println("P2P-CI Version not supported by server");
                JOptionPane.showMessageDialog(new JFrame(), "P2P-CI Version not supported by server", "Unsupported Version", JOptionPane.ERROR_MESSAGE);
            }
        } catch (Exception e) {
        }
        return null;
    } 
   
    public LinkedList<RfcField> list() {
        if (! isConnected) {
            connectToServer();
            if (! isConnected) {
                return null;
            }
        }
        PSRequest request = new PSRequest.PSRequestBuilder().exchangeMethod(PSMethod.LIST).version(Constants.kVersion).hostField(new HeaderField("Host", peerHostName)).portField(new HeaderField("Port", "" + uploadServerPortNumber)).build();
        try {
            //System.out.println(request.getRequestMessage());
            out.writeInt(request.getRequestMessage().length());
            out.writeBytes(request.getRequestMessage());
        } catch (Exception e) {
            e.printStackTrace();
        }

        try {
            byte input[] = new byte[in.readInt()];
            in.readFully(input);
            String message = new String(input);
            PSResponse response = PSResponse.parseFromString(message);
            if (response == null) {
                JOptionPane.showMessageDialog(new JFrame(), "Failed to parse response from server@" + this.serverHostName, "Parse Error", JOptionPane.ERROR_MESSAGE);
            } else if (response.getStatus() == Status.OK) {
                System.out.println("List successful");
                for (int i = 0; i < response.getResponseList().size(); ++i) {
                    System.out.println(response.getResponseList().get(i).getHostName() + " : RFC " + response.getResponseList().get(i).getRfcNumber());
                }
            } else if (response.getStatus() == Status.NOT_FOUND) {
                System.out.println("No list found at server");   
                 JOptionPane.showMessageDialog(new JFrame(), "No list found at server", "List Error", JOptionPane.ERROR_MESSAGE);
            } else if (response.getStatus() == Status.BAD_REQUEST) {
                System.out.println("A Bad Request");
                JOptionPane.showMessageDialog(new JFrame(), "A bad request occurred", "Bad Request", JOptionPane.ERROR_MESSAGE);
            } else if (response.getStatus() == Status.P2P_CI_VERSION_NOT_SUPPORTED) {
                System.out.println("P2P-CI Version not supported by server");
                JOptionPane.showMessageDialog(new JFrame(), "P2P-CI Version not supported by server", "Unsupported Version", JOptionPane.ERROR_MESSAGE);
            }

            return response.getResponseList();
        } catch (Exception e) {
            e.printStackTrace();
        }
        return null;
    } 
   
    private boolean startConnection() {
        //Create a socket connection to the server and 
        //start the upload server on a separate thread
        try {
            serverHostName = Constants.kHostName;
            serverPortNumber = Constants.kPortNumber;
            peerSocket = new Socket(serverHostName, serverPortNumber);
            System.out.println("Peer started connection : " + peerSocket.getInetAddress().getHostAddress());
        } catch (IOException e) {
            JOptionPane.showMessageDialog(new JFrame(), "Could not connect to server on " + Constants.kHostName + " at port " + Constants.kPortNumber, "Connection Error", JOptionPane.ERROR_MESSAGE);
            return false;
        } 
        try {
            in = new DataInputStream(peerSocket.getInputStream());
            out = new DataOutputStream(peerSocket.getOutputStream());
            out.writeInt(this.uploadServer.portNumber);
        } catch (IOException e) {
            JOptionPane.showMessageDialog(new JFrame(), "Could not create input/output streams with the server", "Connection Error", JOptionPane.ERROR_MESSAGE);
            return false;
        }
        return true;
    }

    private void startUploadServer() {
        uploadServer = new UploadServer(rfcMap, uploadServerPortNumber, uploadEditorPane);
        new Thread(uploadServer).start();
    }
    
    private void initializeRfcMap() {
        HashMap<Integer, String> codeMap = null;

        try {
            FileInputStream fis = new FileInputStream("resources/metadata.dat");
            ObjectInputStream ois = new ObjectInputStream(fis);
            codeMap = (HashMap<Integer, String>) ois.readObject();
        } catch (Exception e) {
            e.printStackTrace();
        }
        int filesPerPeer = 20;
        File rfcDirectory = new File("resources/rfc");
        File rfcFiles[] = rfcDirectory.listFiles();

        Random generator = new Random();

        for (int i = 0; i < filesPerPeer;) {
            try {
                int index = generator.nextInt(rfcFiles.length);
                if (! rfcFiles[index].getName().contains(".txt")) {
                    continue;
                }
                Pattern p = Pattern.compile("\\d+");
                Matcher m = p.matcher(rfcFiles[index].getName());
                int number = 0;
                while (m.find()) {
                    number = Integer.parseInt(m.group());
                }
                Peer.testNumber = number;
                Peer.testName = codeMap.get(new Integer(Peer.testNumber));
                
                if (! rfcMap.containsKey(new Integer(number))) {
                    RfcData metadata = new RfcData(number, codeMap.get(new Integer(number)), InetAddress.getLocalHost().getHostName());
                    String content = "";
                    Rfc rfc;
                    try {
                        BufferedReader buf = new BufferedReader(new InputStreamReader(new FileInputStream(rfcFiles[index])));
                        String currentLine;
                        while ((currentLine = buf.readLine()) != null) {
                            content += currentLine + "\n";
                        }
                    } catch (Exception e) {
                    }
                    rfc = new Rfc(metadata, content, new Date(rfcFiles[index].lastModified()));
                    rfcMap.put(new Integer(number), rfc);
                    i++;
                    MySplashScreen.currentMessage = "Loading " + rfcFiles[index].getName();
                    System.out.println(rfcFiles[index].getName());
                }
            } catch (Exception e) {
               e.printStackTrace(); 
            }
        }
        System.out.println("RFC Map initialized at peer");
    }
    
    private int getFirstFreeLocalPort() {
        int portRangeLow = Constants.kPortScanLow;
        int portRangeHigh = Constants.kPortScanHigh;

        for (int i = portRangeLow; i <= portRangeHigh; ++i) {
            try {
                Socket socket = new Socket(InetAddress.getLocalHost().getHostName(), i);
                socket.close();
            } catch (Exception e) {
                return i;
            }
        }
        return -1;
    }
   
    public void closeConnection() {
        if (isConnected) {
            try {
                PSRequest request = new PSRequest.PSRequestBuilder().exchangeMethod(PSMethod.CLOSE).version(Constants.kVersion).hostField(new HeaderField("Host", this.peerHostName)).build();

                out.writeInt(request.getRequestMessage().length());
                out.writeBytes(request.getRequestMessage());
                in.close();
                out.close();
                peerSocket.close();
            } catch (IOException e) {
                e.printStackTrace();
            }
        }
    }
  
    @Override
    public void finalize() {
        this.closeConnection();
    }
   
    public static void testPS() {
        Peer peer = new Peer(null);
        System.out.println("Testing for " + Peer.testNumber + " : " + 
                            Peer.testName);
        //peer.list();
        //peer.closeConnection();
        peer.lookupRfc(Peer.testNumber, Peer.testName);
        //peer.peerSocket.close(); 
        try {
                Thread.sleep(5000);
            } catch (Exception e) {
            }
        peer.closeConnection();
    }
    
    public static void testPP() {
        Peer peer = new Peer(null);
        while (true) {
            try {
                Thread.sleep(5000);
            } catch (Exception e) {
                
            }
        }
    }
   
    public static void main(String[] args) throws Exception {
        //testPP();

        testPS();
    }   
}
